16.ReentrantLock全解读

您所在的位置:网站首页 virtio 源码解读 16.ReentrantLock全解读

16.ReentrantLock全解读

2023-06-12 04:06| 来源: 网络整理| 查看: 265

大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。快来加入我们的Java提桶跑路群:共同富裕的Java人。

经历了AQS的前世和今生后,我们已经知道AQS是Java中提供同步状态原子管理,线程阻塞/唤醒,以及线程排队功能的同步器基础框架。那么我们今天就来学习通过AQS实现的ReentrantLock。按照惯例,先来看3道关于ReentrantLock的常见面试题:

什么是ReentrantLock?ReentrantLock内部原理是怎样的?如何实现可重入性?ReentrantLock和synchronized有什么区别?该如何选择?

接下来,我会尽可能的通过剖析源码的方式为大家解答以上的题目。

ReentrantLock是什么?

ReentrantLock译为可重入锁,在《一文看懂并发编程中的锁》中我们解释过锁的可重入特性:同一线程可以多次加锁,即可以重复进入被锁定的逻辑中。

Doug Lea是这样描述ReentrantLock的:

A reentrant mutual exclusion {@link Lock} with the same basic behavior and semantics as the implicit monitor lock accessed using {@code synchronized} methods and statements, but with extended capabilities.

“A reentrant mutual exclusion Lock”说明ReentrantLock除了具有可重入的特性,还是一把互斥锁。接着看后面的内容,ReentrantLock与使用synchronized方法/语句有相同的基本行为和语义。最后的" but with extended capabilities"则表明了ReentrantLock具有更好的拓展能力。

那么可重入互斥锁就是ReentrantLock的全部吗?别急,我们接着往后看:

The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order.

ReentrantLock提供了公平/非公平两种模式,默认非公平模式,可以通过构造器参数指定公平模式。

好了,目前为止我们已经对ReentrantLock有了比较清晰的认知了,按照《一文看懂并发编程中的锁》中的分类,ReentrantLock本质是互斥锁,具有可重入特性,此外ReentrantLock还实现了公平和非公平两种模式。

ReentrantLock怎么用?

ReentrantLocak的使用非常简单:

ReentrantLock lock = new ReentrantLock(); lock.lock(); // 业务逻辑 lock.unlock();

通过无参构造器创建ReentrantLock对象后,调用lock和unlock进行加锁和解锁的操作。除了无参构造器外,ReentrantLock还提供了一个有参构造器:

// 无参构造器 public ReentrantLock() { sync = new NonfairSync(); } // 有参构造器 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }

FairSync和NonfairSync是ReentrantLock的内部类,可以通过构造器来指定ReentrantLock的公平模式或非公平模式。具体实现我们先按下不表,先来看ReentrantLock中提供的其它方法。

加锁方法

除了常用的lock外,ReentrantLock还提供了3个加锁方法:

// 尝试获取锁 public boolean tryLock(); // 尝试获取锁,否则排队等候指定时间 public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException; // 尝试获取锁 public void lockInterruptibly() throws InterruptedException;

tryLock直接尝试获取锁,特点在于竞争失败时直接返回false,不会进入队列等待。重载方法tryLock(long timeout, TimeUnit unit)增加的在队列中的最大等待时间,如果锁竞争失败,会加入到等待队列中,再次尝试获取锁,直到超时或中断。

lockInterruptibly的特点是,调用thread.interrupt中断线程后抛出InterruptedException异常,结束竞争。虽然lock也允许中断线程,但它并不会抛出异常。

其他方法

除了常用的加锁方法外,ReentrantLock还提供了用于分析锁的方法:

方法声明作用public int getHoldCount()返回当前线程持有锁的次数,即当前线程重入锁的次数public final int getQueueLength()返回等待获取锁的线程数量估算值public final boolean hasQueuedThread(Thread thread)查询当前线程是否在等待获取锁public final boolean hasQueuedThreads()是否有线程在等待获取锁public final boolean isFair()是否为公平锁public boolean isHeldByCurrentThread()当前线程是否持有锁public boolean isLocked()锁是否被线程持有,即锁是否被使用public Condition newCondition()创建条件对象public int getWaitQueueLength(Condition condition)等待在该条件上的线程数量public boolean hasWaiters是否有线程在等待在该条件上 ReentrantLock源码分析

接下来,我们通过源码来分析ReentrantLock的公平/非公平模式,以及重入性的实现原理,并对比不同的加锁方法的实现差异。

ReentrantLock的结构

我们先来来了解下ReentrantLock的结构:

public class ReentrantLock implements Lock, java.io.Serializable { private final Sync sync; // 同步器 abstract static class Sync extends AbstractQueuedSynchronizer {} // 非公平模式同步器 static final class NonfairSync extends Sync {} // 公平模式同步器 static final class FairSync extends Sync {} }

ReentrantLock仅仅实现了Lock接口,并没有直接继承AbstractQueuedSynchronizer,其内部类Sync继承AbstractQueuedSynchronizer,并提供了FairSync和NonfairSync两种实现,分别是公平锁和非公平锁。

公平/非公平模式

我们已经知道,可以指定不同的参数来创建公平/非公平模式的ReentrantLock,反应到源码中是使用不同的Sync的实现类:

public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }

并且在加锁/解锁操作中,均由Sync的实现类完成,ReentrantLock只是对Lock接口的实现:

public class ReentrantLock implements Lock, java.io.Serializable { public void lock() { sync.acquire(1); } public void unlock() { sync.release(1); } }

先来回想下《AQS的今生,构建出JUC的基础》中的acquire方法:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { selfInterrupt(); } } }

AQS自身仅实现了将线程添加到等待队列中的acquireQueued方法,而预留了获取锁的tryAcquire方法。

那么我们不难想到,ReentrantLock的作用机制:继承自AQS的Sync,实现了tryAcquire方法来获取锁,并借助AQS的acquireQueued实现排队的功能,而ReentrantLock的公平与否,与tryAcquire的实现方式是息息相关的。

公平锁FairSync

FairSync非常简单,仅做了tryAcquire方法的实现:

static final class FairSync extends Sync { @ReservedStackAccess protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); // 获取同步状态,AQS实现 int c = getState(); // 判断同步状态 // c == 0时,表示没有线程持有锁 // c != 0时,表示有线程持有锁 if (c == 0) { // hasQueuedPredecessors判断是否有已经在等待锁的线程 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 线程重入,同步状态+1 int nextc = c + acquires; if (nextc < 0) { throw new Error("Maximum lock count exceeded"); } // 更新同步状态 setState(nextc); return true; } return false; } }

当c == 0时,锁未被任何线程持有,通过hasQueuedPredecessors判断是否已经有等待锁的线程,如果没有正在等待的线程,则通过compareAndSetState(0, acquires)尝试替换同步状态来获取锁;当c != 0时,锁已经被线程持有,通过current == getExclusiveOwnerThread判断是否为当前线程持有,如果是则认为是重入,执行int nextc = c + acquires,更新同步状态setState(nextc),并返回成功。

FairSync的公平性体现在获取锁前先执行hasQueuedPredecessors,确认是否已经有线程在等待锁,如果有则tryAcquire执行失败,默默的执行AQS的acquireQueued加入等待队列中即可。

非公平锁NonfairSync

NonfairSync也只是做了tryAcquire的实现,而且还只是掉用了父类的nonfairTryAcquire方法:

static final class NonfairSync extends Sync { protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } abstract static class Sync extends AbstractQueuedSynchronizer { @ReservedStackAccess final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) { throw new Error("Maximum lock count exceeded"); } setState(nextc); return true; } return false; } }

nonfairTryAcquire与FairSync#tryAcquire简直是一模一样,忽略方法声明,唯一的差别就在于,当c == 0时,nonfairTryAcquire并不会调用hasQueuedPredecessors确认是否有线程正在等待获取锁,而是直接通过compareAndSetState(0, acquires)尝试替换同步状态来获取锁。

NonfairSync的不公平体现在获取锁前不会不会确认是否有线程正在等待锁,而是直接获取锁,如果获取失败,依旧会执行AQS的acquireQueued加入等待队列。

可重入性的实现

《AQS的今生,构建出JUC的基础》中,提到过ReentrantLock的重入性依赖于同步状态state作为计数器的特性实现,在公平锁FairSync和非公平锁NonfairSync的实现中我们也看到,线程重入时会执行同步状态+1的操作:

int nextc = c + acquires; setState(nextc);

既然lock操作中有同步状态+1的操作,那么unlock操作中就一定有同步状态-1的操作:

public class ReentrantLock implements Lock, java.io.Serializable { public void unlock() { sync.release(1); } abstract static class Sync extends AbstractQueuedSynchronizer { @ReservedStackAccess protected final boolean tryRelease(int releases) { // 线程退出,同步状态-1 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) { throw new IllegalMonitorStateException(); } boolean free = false; if (c == 0) { // 同步状态为0,锁未被持有,释放独占锁 free = true; setExclusiveOwnerThread(null); } setState(c); return free; } } } public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0){ unparkSuccessor(h); } return true; } return false; } }

tryRelease的实现并不复杂,同步状态-1后,如果同步状态为0,表示锁未被持有,修改锁的独占线程,然后更新同步状态。

我们再来看ReentrantLock的可重入性的实现,是不是非常简单了?判断是否是线程重入依赖的是getExclusiveOwnerThread方法,获取当前独占锁的线程,记录重入次数依赖的是同步状态作为计数器的特性。

现在能够理解为什么ReentrantLock中lock要与unlock操作成对出现了吧?最后,提个小问题,为什么lock和unlock操作中,只有当c == 0时的lock操作需要使用CAS?

加锁方法的差异

我们前面已经了解过ReentrantLock提供的4个加锁方法了,分别是:

public void lock(),最常用的加锁方法,允许中断,但不会抛出异常,加锁失败进入等待队列;public void lockInterruptibly(),允许中断,抛出InterruptedException异常,加锁失败进入队列直到被唤醒或者被中断;public boolean tryLock(),尝试直接加锁,加锁失败不会进入队列,而是直接返回false;public boolean tryLock(long timeout, TimeUnit unit),尝试直接加锁,中断时抛出InterruptedException异常,加锁失败进入队列,直到指定时间内加锁成功,或者超时。 lock与lockInterruptibly

lock方法的调用:

public class ReentrantLock implements Lock, java.io.Serializable { public void lock() { sync.acquire(1); } } public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { selfInterrupt(); } } }

lockInterruptibly方法的调用:

public class ReentrantLock implements Lock, java.io.Serializable { public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } } public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) { throw new InterruptedException(); } if (!tryAcquire(arg)) { doAcquireInterruptibly(arg); } } }

可以看到,差异主要体现在acquireQueued和doAcquireInterruptibly的实现上:

final boolean acquireQueued(final Node node, int arg) { boolean interrupted = false; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; return interrupted; } // 当parkAndCheckInterrupt为true时,修改interrupted标记为中断 if (shouldParkAfterFailedAcquire(p, node)) interrupted |= parkAndCheckInterrupt(); } } catch (Throwable t) { cancelAcquire(node); if (interrupted) selfInterrupt(); throw t; } } private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; return; } // 当parkAndCheckInterrupt为true时,抛出异常 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } catch (Throwable t) { cancelAcquire(node); throw t; } }

从源码上来看,差异体现在对parkAndCheckInterrupt结果的处理方式上,acquireQueued只标记中断状态,而doAcquireInterruptibly直接抛出异常。

tryLock与它的重载方法

public boolean tryLock()的实现非常简单:

public boolean tryLock() { return sync.nonfairTryAcquire(1); }

直接调用Sync#nonfairTryAcquire,在前面非公平锁的内容中我们已经知道nonfairTryAcquire只是进行了一次非公平的加锁尝试,如果没有调用AQS的acquireQueued不会加入到等待队列中。

tryLock的重载方法也并不复杂,按照之前的习惯,应该是有着特殊的acquireQueued实现:

public class ReentrantLock implements Lock, java.io.Serializable { public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } } public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) { throw new InterruptedException(); } return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); } private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout SPIN_FOR_TIMEOUT_THRESHOLD时,认为一次park和upark对性能的影响小于自旋nanosTimeout纳秒;当nanosTimeout < SPIN_FOR_TIMEOUT_THRESHOLD时,认为一次park和upark对性能的影响大于自旋nanosTimeout纳秒。

到这里我们就把4个加锁方法的差异讲完了,大体逻辑是相似的(如,唤醒头节点),只是为了实现某些特性添加了一些细节,大家可以认真阅读源码,很容易就能看出差异。

结语

关于ReentrantLock的内容到这里就结束了,因为已经把AQS的部分单独拆了出来,所以今天并没有太复杂的内容。大家的重点可以放在ReentrantLock是如何借助AQS实现公平/非公平模式,以及可重入的特性上,诸如getHoldCount,isFair这类方法,相信大家已经能够想象到是如何实现的了,可以结合源码验证自己的想法。

最后,希望今天的内容能够帮助你更清晰的理解ReentrantLock,如果文章中出现错误,也希望大家不吝赐教。

好了,今天就到这里了,Bye~~



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3